<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

/*
 *      µtracking is an SQL based, single file Issue Tracking System
 * 
 *      Copyright (c) 2012, Ale Rimoldi <http://ideale.ch/contact/>
 *      All rights reserved.
 *
 *      Redistribution and use in source and binary forms, with or without modification,
 *      are permitted provided that the conditions listed in the license file on
 *      http://code.google.com/p/mutracking/ are met.
 *
 *      It has been inspired by Tiny Issue Tracker (TIT) v0.2
 *      by Jwalanta Shrestha <jwalanta at gmail dot com>
 *
 *      a.l.e has:
 *      - removed some warnings (mostly undefined variables)
 *      - removed all global variables (except for notify which i have not touched yet)
 *      - moved to PDO/prepared statements in order to avoid all possible sql injections
 *      - moved the users to the db (in the future allow some -- external? -- user management?)
 *      - added optional backlinks to the project pages
 *      - moved all the browsing links below the project name
 *      - added a changedtime, updated for comments, and optionally order the list by it
 *      - allow to create a new issue even if an issue is currently being edited
 *      - in the list show the id and not the counter
 *      - optionally allow guests to browse the issues
 *      - users can be disabled by setting the password to an empty string
 *      - moved the configuration to a "static" class instead of global variables
 *      - introduced string templates
 *      - renamed issue > status to issue > resolved
 *      - move it to a simple MVC: better overview of what is done
 *      - refactor the issue tracker code into an issue class
 *      - rewritten everything, doubled the lines of code... and it's still tiny...
 *      - always use id's for foreign keys (users)
 *      - removed the edit link (click on the issue's title to edit)
 *      - found an ascii icon for delete
 *      - my first try with depency injection
 *      - create a controller and a view for each class
 *      - install only with ?install and if Configuration::install is true
 *      - make it translatable (optional external csv file)
 *      a.l.e plans:
 *      - allow registration (error feedback (existing username, non matching password), stearing the registration from the configuration, implement the approve process (approve this comment, approve the user, delete comment, delete the user), approve-treshold)
 *      - reenable, simplify and improve notifications
 *      - separate the notification from the Issue class
 *      - add to the notifications a "show modified issues": bold if modified since last seen or show list of
 *        the last actions (with link to the issue)
 *      - email if password forgotten
 *      - remove the password from the session (use a hash instead)
 *      - assign the issues to users (or only to self?)
 *      - set list of tags allowed for title, descriptions and comments (sanitizing the input)
 *      - show / hide resolved issues (instead of two different lists)
 *      - optionally allow attachments to bugs (and notes? do it as a plugin?)
 *      - pagination if more than n issues
 *      - make it also work with mysql (should be easy)
 *      - add a dashboard for admin users (accept self-registered users and their posts)
 *      - guests and "non accpeted" users can post but entries are only seen by registered users/admins
 *        (accept / mark as spam)
 *      - add a simple captcha for registration and anonymous posting
 *      - a registred user needs $treshold accepted posts to directly post (exception for the admins)
 *      - create the types question / idea / problem / chat
 *      - list of followed issues
 *      - replce the priority by something more meaningful (deadline + complexity? no deadline = normal
 *        priority; deadline close = high priority)
 *      - find a concept to make this app extendable (plugins? in the same file and/or external?)
 *      - build other apps on this code (http://www.aypwip.org/webnote/)
 *      - user management?
 *      - do we need milestones? and voting? rss?
 *      - use auth or auth tables from other packages (wp, ...)
 *      - embed in other apps (wp plugin?)
 */

///////////////////
// CONFIGURATION //
///////////////////

class Configuration {
    public static $install = true; // set it to false once μtracking is installed
    public static $project = array (
        'title' => 'My Project',        // Project Title
        'email' => 'noreply@test.com',  // "From" email address for notifications
        'navigation' => array(/* "http://myproject.org" => "Home" */),  // Backlinks to the project pages
    );
    public static $captcha = array(
        'question' => '3 + 2',
        'answer' => '5',
    );
    public static $authentication = array(
        'guest' => false, // allow guests to see the issues
        'registration' => true, // allow people to register themselves
        'password_recover' => true, // allow users to recover their password
    );
    public static $user = array(
        'install' => array (
            array("admin", "admin","admin@test.com", 1),
            array("user", "user","user@test.com", 0),
        ),
        'notification' => true, // allow notification at all
        /**
         * Automatically follow issues
         * ( ) Never
         * ( ) Only own issues
         * ( ) All new issues
         * 
         * Get notification on followed issues:
         * ( ) Never
         * ( ) Creation and solving
         * ( ) All changes
         */
        'notification_default' => array (
            'follow_all' => false,
            'follow_own' => false,
            'notify_lifecycle' => false,
            'notify_all' => false,
        ),
    );
    public static $db = array (
        'sqlite' => 'sqlite/tit.db', // If the sqlite file doesn't exist, a new one will be created. Make sure the folder is writable
        // mysql access is not implemented, yet (only one type can be enabled at a time)
        // 'mysql' => 'user:password@localhost/tit',
    );
    public static $issue = array(
        'order_by_create' => false,      //	Manage the way the list is displayed (create / update)
        'date_format' => 'd.m.Y H:i',   //  Data format in the list
    );
} // Configuration

////////////////////////////////////////////////////////////////
// DO NOT EDIT BEYOND THIS IF YOU DONT KNOW WHAT YOU'RE DOING //
////////////////////////////////////////////////////////////////

// debug('_POST', $_POST);
// debug('_GET', $_GET);

$environment = new Environment();
$environment->initialize();

if ($environment->is('install') && Configuration::$install) {
    $user->install(Configuration::$user['install']);
    $issue->install();
} elseif (!file_exists(Configuration::$db['sqlite'])) {
    echo('<html><head><title>μtracking is not installed</title></head><body><p><a href="'.$_SERVER['REQUEST_URI'].'?install">Install μtracking.</a></p></body></html>');
}

$db = new DB();
$db->initialize(Configuration::$db['sqlite']);

session_start();
// debug('_SESSION', $_SESSION);

// Translate::initialize('mutracking_de.csv');

$user = new User();
$user->set_db($db);
$user->set_environment($environment);

$authentication = new Authentication();
$authentication->set_environment($environment);
$authentication->set_user($user);
$authentication->run();

$user->set_authentication($authentication);

$issue = new Issue();
$issue->set_db($db);
$issue->set_environment($environment);
$issue->set_user($user);
$issue->set_authentication($authentication);
$issue->run();

$user->run();

$authentication->render();
$issue->render();

$template = new Template();

$template_menu = <<<EOT
    <?php foreach (\$navigation as \$key => \$value) {echo('<a href="'.\$key.'" alt="'.\$value.'">'.\$value.'</a> | ');} ?>
    <?php echo(\$authentication); ?>
EOT;

$template_mutracking = <<<EOT
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <title><?php echo \$title, " - Issue Tracker"; ?></title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8" />
    <style>
        body { font-family: sans-serif; font-size: 11px; background-color: #aaa;}
        a, a:visited{color:#004989; text-decoration:none;}
        a:hover{color: #666; text-decoration: underline;}
        label{ display: block; font-weight: bold;}
        table{border-collapse: collapse;}
        th{text-align: left; background-color: #f2f2f2;}
        tr:hover{background-color: #f0f0f0;}
        #menu{float: right;}
        #container{width: 760px; margin: 0 auto; padding: 20px; background-color: #fff;}
        #footer{padding:10px 0 0 0; margin-top: 20px; text-align: center; border-top: 1px solid #ccc;}
        #create{padding: 15px; background-color: #f2f2f2;}
        .issue{padding:10px 20px; margin: 10px 0; background-color: #f2f2f2;}
        .comment{padding:5px 10px 10px 10px; margin: 10px 0; border: 1px solid #ccc;}
        .comment-meta{color: #666;}
        .p1, .p1 a{color: red;}
        .p3, .p3 a{color: #666;}
        .hide{display:none;}
        .left{float: left;}
        .right{float: right;}
        .clear{clear:both;}
        
    </style>
</head>
<body>
    <div id="container">
        <div id="menu">
            <?php echo(\$menu) ?>
        </div>
        <h1><?php echo \$title; ?></h1>
        <div>
            <?php echo(\$action) ?>
        </div>
        <?php echo(\$content); ?>
        <div id="footer">
            <?php echo(tr('Powered by %s', '<a href="http://code.google.com/p/mutracking/" alt="mutracking" target="_blank">μtracking</a>')); ?>
        </div>
    </div>
</div>
</body>
</html>
EOT;

echo(
    $template->
        set('title', Configuration::$project['title'])->
        set('menu',
            $template->
                set('navigation', Configuration::$project['navigation'])->
                set('authentication', $authentication->get_view('link'))->
                fetch($template_menu)
        )->
        set('action', $issue->get_view('action'))->
        set('content',
            $authentication->get_view('form').
            $issue->get_view('edit').
            $issue->get_view('list').
            $issue->get_view('show')
        )->
        fetch($template_mutracking)
);

/////////////////// ///////////
// MODEL / VIEW / CONTROLLER //
///////////////////////////////

// manage the authentication actions

class Issue {
    protected $environment = null;
    public function set_environment($environment) {$this->environment = $environment;}
    protected $db = null;
    public function set_db($db) {$this->db = $db;}
    protected $user = null;
    public function set_user($user) {$this->user = $user;}
    protected $authentication = null;
    public function set_authentication($authentication) {$this->authentication = $authentication;}

    protected $view = array('show' => false, 'edit' => false, 'list' => false, 'list_resolved' => false);
    protected $view_html = array();
    public function set_view($field) {$this->view[$field] = true;}
    public function get_view($field) {return array_key_exists($field, $this->view_html) ? $this->view_html[$field] : '';}

    protected $issue = array();
    protected $comment = null;
    protected $list = null;
    public function install() {
        $this->db->execute('CREATE TABLE issue (id INTEGER PRIMARY KEY, title TEXT, description TEXT, user_id INTEGER, resolved INTEGER, priority INTEGER, createtime DATETIME, changetime DATETIME)');
        $this->db->execute('CREATE TABLE comment (id INTEGER PRIMARY KEY, issue_id INTEGER, user_id INTEGER, description TEXT, createtime DATETIME)');
        $this->db->execute('CREATE TABLE issue_notification (issue_id INTEGER, user_id INTEGER)');
    }
    public function read($id = null) {
        $result = false;
        if (!isset($id)) {
            $id = $this->environment->get('id');
        }
        $this->issue = $this->db->fetch("SELECT issue.id, title, description, username, resolved, priority, createtime FROM issue JOIN user ON user_id = user.id WHERE issue.id=:id", array(':id' => $id));
        if (!empty($this->issue)) {
            $result = true;
        }
        return $result;
    } // Issue::read()
    public function set() {
        $result = false;
        // debug('id', $id);
        $title = trim($this->environment->get('title'));
        $id = trim($this->environment->get('id'));
        if ($title !== '') {
            if ($id == '') {
                // create
                $this->db->execute(
                    "INSERT INTO issue (title, description, user_id, priority, resolved, createtime, changetime) values(:title, :description, :user_id, 2, 0, :now, :now)",
                    array('title' => $title, 'description' => $this->environment->get('description'), 'user_id' => $this->user->get_id(), 'now' => date('Y-m-d H:i:s'))
                );
                $id = $this->db->get_last_insert_id();
                $email = $this->send_notification(
                    $this->user->get_email_notify_new(),
                    "[".Configuration::$project['title']."] New issue created: ".$title,
                    "New issue created by ".$this->user->get_username()."\r\nTitle: ".$title."\r\nURL: http://{$_SERVER['HTTP_HOST']}{$_SERVER['PHP_SELF']}?action=issue_show&id=$id"
                );
                $this->set_follow($id, ($this->user->has_follow_own() ? array($this->user->get_id()) : array()) + $this->user->get_id_notify_new());
                $result = true;
            } else { // if id == ''
                // edited
                $this->db->execute(
                    "UPDATE issue SET title=:title, description=:description, changetime=:now WHERE id=:id",
                    array('title' => $title, 'description' => $this->environment->get('description'), 'id' => $id, 'now' => date("Y-m-d H:i:s"))
                ); // edit
                $email = $this->send_notification(
                    $this->user->get_email_notify_change($id),
                    "[".Configuration::$project['title']."] Updated issue: ".$title,
                    "Issue changed by ".$this->user->get_username()."\r\nTitle: ".$title."\r\nURL: http://{$_SERVER['HTTP_HOST']}{$_SERVER['PHP_SELF']}?action=issue_show&id=$id"
                );
                $result = true;
            } // else id == ''
            $this->read($id);
        } // if $title
        return $result;
    } // Issue::set()
    public function get_list($filter = array()) {
        $result = array();
        $filter = $filter + array('resolved' => false, 'assigned_to_me' => false);
        if (!isset($this->list)) {
            $this->list = $this->db->fetch_all("SELECT issue.id, title, description, username AS user, resolved, priority/*, notify_emails*/, createtime, changetime FROM issue AS issue JOIN user AS user ON user_id = user.id WHERE resolved=".($filter['resolved'] ? '1' : '0')." ORDER BY /* priority, */ ".(Configuration::$issue['order_by_create'] ? 'createtime' : 'changetime')." DESC");
        }
        // debug('issue list', $this->list);
        return $this->list;
    } // Issue::get_list()
    public function delete($id = null) {
        $result = false;
        if (is_null($id)) {
            $id = $this->environment->get('id');
        }
        // only the issue creator or admin can delete an existing issue
        if ($this->read($id) && ($this->user->is_admin() || ($this->issue['username'] == $this->user->get_username()))) {
            $this->db->execute("DELETE FROM comment WHERE id=:id", array(':id' => $id));
            $this->db->execute("DELETE FROM issue WHERE id=:id", array(':id' => $id));
            if ($this->db->get_affected_rows() == 1) {
                $result = true;
            }
            // @todo: notify
        }
        return $result;
    } // Issue::delete()
    public function set_priority($id = null, $priority = null) {
        $result = false;
        if (is_null($id)) {
            $id = $this->environment->get('id');
            $priority = $this->environment->get('priority');
        }
        if (($priority >= 1) && ($priority <= 3)) {
            if ($this->read($this->environment->get('id'))) {
                $this->db->execute("UPDATE issue SET priority=:priority WHERE id = :id", array(':priority' => $priority, ':id' => $id));
                $this->issue['priority'] = $priority;
                $result = true;
            }
        }
        return $result;
    } // Issue::set_priority()
    public function set_watch($id, $watch) {
        if ($watch == 1) {
            $this->db->execute("INSERT issue_notification (issue_id, user_id) values (:issue_id, :user_id)", array(':issue_id' => $id, ':user_id' => $this->user->get_id()));
        } else {
            $this->db->execute("DELETE from issue_notification WHERE issue_id = :issue_id AND user_id = :user_id)", array(':issue_id' => $id, ':user_id' => $this->user->get_id()));
        }
    }
    public function set_resolved($id = null, $resolved = null) {
        $result = false;
        if (is_null($id)) {
            $id = $this->environment->get('id');
            $resolved = $this->environment->get('resolved');
        }
        $this->db->execute("UPDATE issue SET resolved=:resolved, changetime=:now  WHERE id=:id", array(':id' => $id, ':resolved' => $resolved, 'now' => date("Y-m-d H:i:s")));
        $result = $this->db->get_affected_rows() == 1;
        return $result;
    }
    public function add_comment($id = null) {
        $result = false;
        if (is_null($id)) {
            $id = $this->environment->get('issue_id');
            $this->read($id);
        }
        if (!empty($this->issue)) {
            $description = trim($this->environment->get('description'));
            if ($description != '') {
                $this->db->execute("UPDATE issue SET changetime=:now WHERE id=:id", array(':now' => date("Y-m-d H:i:s"), ':id' => $this->issue['id'])); // update change time
                if ($this->db->get_affected_rows() == 1) {
                    $this->db->execute(
                        "INSERT INTO comment (issue_id, description, user_id, createtime) VALUES (:issue_id, :description, :user_id, :now)", array(
                        ':issue_id' => $this->issue['id'],
                        ':description' => $description,
                        ':user_id' => $this->user->get_id(),
                        ':now' => date("Y-m-d H:i:s")
                    )); // create
                    $result = true;
                }
            } // if description
        } // if id
        return $result;
    } // Issue::add_comment()
    public function delete_comment($comment_id = null) {
        $result = false;
        $issue_id = null;
        $user_id = null;
        if (!isset($comment_id)) {
            $comment_id = intval($this->environment->get('comment_id'));
        }
        if ($comment_id > 0) {
            if ($row = $this->db->fetch("SELECT issue_id, user_id FROM comment WHERE id=?", $comment_id)) {
                $issue_id = $row['issue_id'];
                $user_id = $row['user_id'];
            }
        }
        // only comment poster or admin can delete comment
        if (($issue_id > 0) && ($this->user->is_admin() || ($user_id == $this->user->get_id()))) {
            $this->db->execute("DELETE FROM comment WHERE id=?", $comment_id);
            $result = true;
            if ($issue_id > 0) {
                $this->read($issue_id);
            }
        }
        return $result;
    } // Issue::delete_comment()
    public function get_comment_list() {
        if (!isset($this->comment) && isset($this->issue)) {
            $this->comment = $this->db->fetch_all("SELECT comment.id, username, description, createtime FROM comment JOIN user ON user.id = user_id WHERE issue_id=:issue_id ORDER BY createtime ASC", array(':issue_id' => $this->issue['id']));
        }
        return $this->comment;
    } // Issue::get_comment_list()
    public function set_follow($issue_id, $user_id = null) {
        if (is_null($user_id)) { $user_id = $this->user->get_id(); }
	if (!is_array($user_id)) { $user_id = array($user_id); }
	foreach ($user_id as $item) {
		$this->db->execute(
		    "INSERT INTO issue_notification (issue_id, user_id) values(:issue_id, :user_id)",
		    array('issue_id' => $issue_id, 'user_id' => $item)
		);
	}
    } // Issue::set_follow()
    public function send_notification($email, $subject, $notification) {
        debug('implement the delivering of a notification');
    } // Issue::send_notification()
    public function run() {
        if ($this->authentication->is_authenticated()) {
            switch ($this->environment->get('action')) {
                case 'issue_show' :
                    $this->read() ?  $this->set_view('show') : $this->set_view('list') ; 
                break;
                case 'issue_list_resolved' :
                    $this->set_view('list');
                    $this->set_view('list_resolved');
                break;
                case 'issue_write' :
                    $this->set() ?  $this->set_view('show') : $this->set_view('list') ; 
                break;
                case 'issue_delete' :
                    $this->delete();
                    $this->set_view('list'); 
                break;
                case 'issue_priority' :
                    $this->set_priority() ?  $this->set_view('show') : $this->set_view('list') ; 
                break;
                case 'issue_watch' :
                    $this->set_watch() ?  $this->set_view('show') : $this->set_view('list') ; 
                break;
                case 'issue_assign' :
                    $this->set_assign() ?  $this->set_view('show') : $this->set_view('list') ; 
                break;
                case 'issue_resolved' :
                    $this->set_resolved() ?  $this->read() && $this->set_view('show') : $this->set_view('list') ; 
                break;
                case 'issue_comment_add' :
                    $this->add_comment() ?  $this->set_view('show') : $this->set_view('list') ; 
                break;
                case 'issue_comment_delete' :
                    $this->delete_comment() ?  $this->set_view('show') : $this->set_view('list') ; 
                break;
                default :
                    $this->set_view('list');
            }
            $this->set_view('edit');
        } elseif (Configuration::$authentication['guest']) {
            switch ($this->environment->get('action')) {
                case 'issue_show' :
                    $this->read() ?  $this->set_view('show') : $this->set_view('list') ; 
                break;
                case 'issue_list_resolved' :
                    $this->set_view('list');
                    $this->set_view('list_resolved');
                break;
                default :
                    $this->set_view('list');
            }
        }
    } // Issue::run()
    public function render() {
        $template = new Template();

        $template_action = <<<EOT
            <?php if (\$authenticated) : ?>
            <a href="#" onclick=" document.getElementById('edit_id').value=''; document.getElementById('title').value=''; document.getElementById('description').value = ''; document.getElementById('create').className='';document.getElementById('title').focus();">Create Issue</a> |
            <?php endif; ?>
            <?php if (\$guest || \$authenticated) : ?>
            <a href="<?php echo(\$self_http); ?>" alt="Active Issues">Active Issues</a> | 
            <a href="<?php echo(\$self_http); ?>?action=issue_list_resolved" alt="Resolved Issues">Resolved Issues</a>
            <?php endif; ?>
EOT;
        $this->view_html['action'] = $template->
            set('self_http', $_SERVER['PHP_SELF'])->
            set('authenticated', $this->authentication->is_authenticated())->
            set('guest', Configuration::$authentication['guest'])->
            fetch($template_action);


        if ($this->view['list']) {
            $template_list = <<<EOT
            <div id="list">
            <h2><?php \$title ?></h2>
                <table border=1 cellpadding=5 width="100%">
                    <tr>
                        <th width="5%">No.</th>
                        <th width="40%">Title</th>
                        <th width="15%">Created by</th>
                        <th width="20%">Date</th>
                        <th width="5%%">Watch</th>
                        <th width="15%">Actions</th>
                    </tr>
                
                    <?php
                    \$count=1;
                    foreach (\$issues as \$issue){
                        \$count++;
                        echo "<tr class='p{\$issue['priority']}'>\n"; 
                        echo "<td>{\$issue['id']}</td>\n";
                        echo "<td><a href='?action=issue_show&amp;id={\$issue['id']}'>{\$issue['title']}</a></td>\n";
                        echo "<td>{\$issue['user']}</td>\n";
                        echo "<td>".date(\$date_format, strtotime(\$issue[\$date_field]))."</td>\n";
                        echo "<td>";
                        /* if (\$user_authenticated) echo (!empty(\$user_email) && strpos(\$issue['notify_emails'], \$user_email)!==FALSE?"✓":""); */
                        echo "</td>\n";
                        echo("<td>");
                        if (\$user_admin || (\$user_username == \$issue['user'])) echo "<a href='?action=issue_delete&amp;id={\$issue['id']}' onclick='return confirm(\"Are you sure? All comments will be deleted too.\");'>✗</a>";
                        echo "</td>\n";
                        echo "</tr>\n";
                    }
                    
                    ?>

                </table>
            </div>
EOT;
            $this->view_html['list'] = $template->
                set('title', ($this->view['list_resolved'] ? "Resolved " : '' ).'Issues')->
                set('date_format', Configuration::$issue['date_format'])->
                set('date_field', Configuration::$issue['order_by_create'] ? 'createtime' : 'changetime')->
                set('user_authenticated', $this->authentication->is_authenticated())->
                set('user_username', $this->user->get_username())->
                set('user_admin', $this->user->is_admin())->
                set('user_email', $this->user->get_email())->
                set('issues', $this->get_list(array('resolved' => $this->view['list_resolved'])))->
                fetch($template_list);
        } if ($this->view['show']) {
            $template_detail = <<<EOT
            <div id="show">
                <div class="issue">
                    <?php if (\$user_authenticated) : ?>
                    <div style="float:right;"><a href="#" onclick="document.getElementById('create').className=''; document.getElementById('edit_id').value='<?php echo(\$issue['id']); ?>'; document.getElementById('title').value='<?php echo(stripslashes(\$issue['title'])); ?>'; document.getElementById('description').value='<?php echo(stripslashes(\$issue['description'])); ?>'; document.getElementById('title').focus();">[Edit Issue]</a></div>
                    <?php endif; // is_authenticated() ?>
                    <h2><?php echo(htmlentities(stripslashes(\$issue['title']),ENT_COMPAT,"UTF-8")) ?></h2>
                    <p><?php echo str_replace("\n","<br />",htmlentities(stripslashes(\$issue['description']),ENT_COMPAT,"UTF-8")) ?></p>
                </div>
                <?php if (\$user_authenticated) : ?>
                <div class='left'>
                    Priority <select name="priority" onchange="location='<?php echo \$_SERVER['PHP_SELF']; ?>?action=issue_priority&id=<?php echo \$issue['id']; ?>&priority='+this.value">
                        <option value="1"<?php echo (\$issue['priority']==1?"selected":""); ?>>High</option>
                        <option value="2"<?php echo (\$issue['priority']==2?"selected":""); ?>>Medium</option>
                        <option value="3"<?php echo (\$issue['priority']==3?"selected":""); ?>>Low</option>
                        
                    </select>
                </div>

                <div class='left'>
                    <form method="post" action="<?php echo(\$_SERVER['PHP_SELF']); ?>">
                        <input type="hidden" name="id" value="<?php echo \$issue['id']; ?>" />
                        <input type="hidden" name="action" value="issue_resolved" />
                        <input type="hidden" name="resolved" value="<?php echo(\$issue['resolved']==1?"0":"1") ?>" />
                        <input type="submit" name="mark<?php echo(\$issue['resolved']==1?"unsolved":"solved") ?>" value="Mark as <?php echo(\$issue['resolved']==1?"Unsolved":"Solved") ?>" />
                        <?php 
                            /*
                            if (strpos(\$issue['notify_emails'], \$user_email)===FALSE)
                                echo "<input type='submit' name='watch' value='Watch' />\n";
                            else
                                echo "<input type='submit' name='unwatch' value='Unwatch' />\n";
                                [X]
                            */
                        ?>
                    </form>
                </div>	
                <?php endif; ?>
                <div class='clear'></div>
                <div id="comments">
                <?php echo(\$comment); ?>
                </div>
            </div>
EOT;
            $template_comment = <<<EOT
            <h3>Comments</h3>
            <?php foreach (\$comments as \$comment) : ?>
                <div class='comment'>
                    <p><?php echo(str_replace("\n","<br />", htmlentities(stripslashes(\$comment['description']), ENT_COMPAT, 'UTF-8'))) ?></p>
                    <div class='comment-meta'><em><?php echo(\$comment['username']) ?></em> on <em><?php echo(\$comment['createtime']) ?></em>
                    <?php if (\$user_admin || \$user_username == \$comment['username']) : ?>
                        <span class="right"><a href="<?php echo(\$_SERVER['PHP_SELF']) ?>?action=issue_comment_delete&comment_id=<?php echo(\$comment['id']) ?>" onclick="return confirm('Are you sure?');">Delete</a></span>
                    <?php endif; ?>
                    </div>
                </div>
            <?php endforeach; ?>
            <?php if (\$user_authenticated) : ?>
            <div id="comment-create">
                <h4>Post a comment</h4>
                <form method="post" action="<?php echo(\$_SERVER['PHP_SELF']); ?>">
                    <input type="hidden" name="action" value="issue_comment_add" />
                    <input type="hidden" name="issue_id" value="<?php echo(\$issue['id']) ?>" />
                    <textarea name="description" rows="5" cols="50"></textarea>
                    <label></label>
                    <input type="submit" name="createcomment" value="Comment" />
                </form>			
            </div>		
            <?php endif; ?>
EOT;
            $this->view_html['show'] = $template->
                // set('title', ($this->get_view('list_resolved') ? "Resolved " : '' ).'Issues')->
                set('title', 'Issues')->
                set('issue', $this->issue)->
                set('user_admin', $this->user->is_admin())->
                set('user_authenticated', $this->authentication->is_authenticated())->
                set('user_email', $this->user->get_email())->
                set('comment', $template->
                    set('issue', $this->issue)->
                    set('comments', $this->get_comment_list())->
                    set('user_admin', $this->user->is_admin())->
                    set('user_authenticated', $this->authentication->is_authenticated())->
                    set('user_username', $this->user->get_username())->
                    fetch($template_comment)
                )->
                fetch($template_detail);
        }
        if ($this->view['edit']) {
            /* @todo: find a way to change the label of the button from js  from save to create and viceversa */
            $template_edit = <<<EOT
                <div id="create" class='hide'>
                    <a href="#" onclick="document.getElementById('create').className='hide';" style="float: right;">[Close]</a>
                    <form method="post" action="<?php echo(\$_SERVER['PHP_SELF']); ?>">
                        <input type="hidden" name="id" id="edit_id" value="<?php echo(\$id); ?>" />
                        <input type="hidden" name="action" value="issue_write" />
                        <label>Title</label><input type="text" size="50" name="title" id="title" value="" /> 
                        <label>Description</label><textarea name="description" rows="5" cols="50" id="description"></textarea>
                        <label></label><input type="submit" name="createissue" value="Save" />
                    </form>
                </div>
EOT;
            $this->view_html['edit'] = $template->
                set('id', isset($this->issue['id']) ? $this->issue['id'] : '')->
                fetch($template_edit);
        }
    } // Issue::render()
} // Issue

class Authentication {
    protected $environment = null;
    public function set_environment($environment) {$this->environment = $environment;}
    protected $user = null;
    public function set_user($user) {$this->user = $user;}
    protected $view = array('form' => false, 'form_error' => false, 'link' => false, 'logout_link' => false);
    protected $view_html = array();
    public function set_view($field) {$this->view[$field] = true;}
    public function get_view($field) {return array_key_exists($field, $this->view_html) ? $this->view_html[$field] : '';}
    protected $authentication = null;

    public function login($username, $password) {
        // debug('username/password', $username.'/'.$password);
        return $this->authenticate($username, md5($password));
    } // Authentication::login()
    public function logout() {
        $_SESSION['mu_user']='';	// username
        $_SESSION['mu_password']='';	// password
    } // Authentication::logout()
    public function is_authenticated($username = null, $password = null) {
        // debug('_SESSION', $_SESSION);
        if (is_null($username) && array_key_exists('mu_user', $_SESSION)) {
            $username = $_SESSION['mu_user'];
            $password = $_SESSION['mu_password'];
        }
        return isset($this->authentication) || $this->authenticate($username, $password);
    } // Authentication::is_authenticated()
    public function authenticate($username, $password) {
        $result = false;
        // debug('username/password', $username.'/'.$password);
        if (($username != '') && ($password != '')) {
            $user = $this->user->get($username, $password);
            if (!empty($user)) {
                $result = true;
                $this->authentication = array('username' => $user['username']);
                if (!array_key_exists('mu_user', $_SESSION) || ($_SESSION['mu_user'] == '')) {
                    $_SESSION['mu_user'] = $user['username'];
                    $_SESSION['mu_password'] = $user['password'];
                }
            } // user
        } // if $username
        // debug('_SESSION', $_SESSION);
        return $result;
    } // Authentication::authenticate()

    public function run() {
        if ($this->environment->is('action', 'authentication_login')) {
            if (!$this->login($this->environment->get('username'), $this->environment->get('password'))) {
                $this->set_view('form');
                if ($this->environment->is('user')) {
                    $this->set_view('form_error');
                }
            }
        } elseif ($this->environment->is('action', 'authentication_logout')) {
            $this->logout();
        }
        if (!$this->is_authenticated() && !Configuration::$authentication['guest']) {
            $this->set_view('form');
        }
    }

    public function render() {
        $template = new Template();
        // debug('view', $this->view);
        $template_link = <<<EOT
            <?php if (\$user_authenticated) : ?>
            <a href="<?php echo \$_SERVER['PHP_SELF']; ?>?action=authentication_logout" alt="Logout">Logout [<?php echo \$username ?>]</a>
            <?php else: ?>
            <a href="<?php echo \$_SERVER['PHP_SELF']; ?>?action=authentication_login" alt="Login">Login</a>
            <?php endif; ?>
EOT;
        $this->view_html['link'] = $template->
            set('username', $this->authentication['username'])->
            set('user_authenticated', $this->is_authenticated())->
            fetch($template_link);
        if ($this->view['form']) {
            $template_login_form = <<<EOT
                <?php if (!empty(\$message)) {echo("<p>\$message</p>");} ?>
                <form method="post" action="<?php echo(\$_SERVER['PHP_SELF']); ?>" name="login_form">
                <input type="hidden" name="action" id="action" value="authentication_login"/>
                <div id="login_username">
                <label>Username</label><input type="text" name="username" />
                </div>
                <div id="login_password">
                <label>Password</label><input type="password" name="password" />
                </div>
                <div id="login_password_repeat" class="hide">
                <label>Repeat Password</label><input type="password" name="password_repeat" />
                </div>
                <div id="login_email" class="hide">
                <label>Email</label><input type="text" name="email" />
                </div>
                <label></label><input type="submit" name="login" id="login" value="Login" />
                <?php if (\$password_recover) {echo('<label></label><a href="" onclick="document.getElementById(\'login_username\').className=\'hide\'; document.getElementById(\'login_email\').className=\'\'; document.getElementById(\'login_password\').className=\'hide\'; document.getElementById(\'login_password_repeat\').className=\'hide\'; document.getElementById(\'login\').value=\'Recover\'; return false;">Recover password</a>');} ?>
                <?php if (\$registration) {echo('<label></label><a href="" onclick="document.getElementById(\'login_username\').className=\'\'; document.getElementById(\'login_email\').className=\'\'; document.getElementById(\'login_password\').className=\'\'; document.getElementById(\'login_password_repeat\').className=\'\'; document.getElementById(\'action\').value=\'user_register\'; document.getElementById(\'login\').value=\'Register\'; return false; ">Register</a>');} ?>
                </form>
EOT;
            $this->view_html['form'] = $template->
                set('message', ($this->view['form_error'] ? 'Invalid username or password' : ''))->
                set('password_recover', Configuration::$authentication['password_recover'])->
                set('registration', Configuration::$authentication['registration'])->
                fetch($template_login_form);
        }
    } // Authentication::render()
} // Authentication

class User {
    protected $view = null;
    protected $db = null;
    public function set_db($db) {$this->db = $db;}
    protected $environment = null;
    public function set_environment($environment) {$this->environment = $environment;}
    protected $authentication = null;
    public function set_authentication($authentication) {$this->authentication = $authentication;}

    protected $user = null;
    public function is_admin() {return isset($this->user) && ($this->user['admin'] == 1);}
    public function is_username($username) {return isset($this->user) && ($this->user['username'] == $username);}
    public function get_username() {return isset($this->user) ? $this->user['username'] : '';}
    public function get_id() {return isset($this->user) ? $this->user['id'] : '';}
    public function get_email() {return isset($this->user) ? $this->user['email'] : '';}
    public function install($user = array()) {
        $success = $this->db->execute('CREATE TABLE user (id INTEGER PRIMARY KEY, username TEXT, password TEXT, email TEXT, admin INTEGER, approved INTEGER)');
        // debug('success', $success);
        if ($success) {
            $this->db->execute('CREATE TABLE user_notification (user_id INTEGER, follow_own INTEGER, follow_all INTEGER, notify_lifecycle INTEGER, notify_all INTEGER)');
            $db_query_user = 'INSERT INTO user (username, password, email, admin) values (:username, :password, :email, :admin, :approved)';
            $db_query_notification = 'INSERT INTO user_notification (user_id, follow_own, follow_all, notify_lifecycle, notify_all) values (:user_id, :follow_own, :follow_all, :notify_lifecycle, :notify_all)';
            
            foreach ($user as $item) {
                // debug('db_query', $db_query);
                $db_result = $this->db->execute($db_query_user, array(':username' => $item[0], ':password' => md5($item[1]), ':email' => $item[2], ':admin' => $item[3], ':approved' => 1));
                $user_id = $this->db->get_last_insert_id();
                if ($db_result) {
                    $this->db->execute($db_query_notification, array(':user_id' => $user_id));
                }
            }
        }
    } // User::install()
    public function register_controller(& $controller) {
        if ($this->is_authenticated()) {
            $this->view->set('login_link_logout');
        } else {
            // @todo: find a way not to show the issues list if the login form is shown
            if (Configuration::$user['guest']) {
                $this->view->set('login_link');
            } else {
                $controller->register('action', 'user_recover', array($this->view, 'set'), 'login_form');
            }
            // @todo: how can we show the login form?
            $controller->register('action', 'user_login', array($this->view, 'set'), 'login_form');
            $controller->register('action', 'user_recover', array($this->view, 'set'), 'login_form');
            $controller->register('action', 'user_recover', array($this, 'recover'));
            $controller->register('action', 'user_recover', array($this->view, 'set'), 'login_form');
        }
    } // User::register_controller()

    public function exists($username) {
        $result = false;
        $db_row = $this->db->fetch("SELECT id FROM user WHERE username=:username", array(':username' => $username));
        if (!empty($db_row)) {
            $result = true;
        }
        return $result;
    }

    public function add($username, $password, $email, $admin, $active) {
    } // User::add()

    public function register($username = null, $password = null, $email = null) {
        debug('_POST', $_POST);
        $result = false;
        if (is_null($username)) {
            $username = $this->environment->get('username');
            $password = $this->environment->get('password');
            $password_repeat = $this->environment->get('password_repeat');
            if ($password != $password_repeat) {
                $password = '';
            }
            $email = $this->environment->get('email');
        }
        if (($username != '') && ($password != '') && ($email != '') && !$this->exists($username)) {
            debug('username', $username);
                $this->db->execute(
                    "INSERT INTO user (username, password, email, admin, approved) values (:username, :password, :email, 0, 0)",
                    array('username' => $username, 'password' => md5($password), 'email' => $email)
                );
            $result = true;
        }
        return $result;
    } // User::register()

    public function get($id_or_username = null, $password = null) {
        if (is_null($this->user)) {
            // debug('id_or_username/password', $id_or_username.'/'.$password);
            $this->read($id_or_username, $password);
        }
        return $this->user;
    }
    public function read($id_or_username = null, $password = null) {
        $result = false;
        if (!isset($id_or_username)) {
            $id_or_username = $this->environment->get('user_id');
        }
        if (isset($password)) {
            $db_row = $this->db->fetch("SELECT id, username, password, email, admin, follow_own, follow_all, notify_lifecycle, notify_all FROM user JOIN user_notification ON id = user_id WHERE username=:username AND password=:password", array(':username' => $id_or_username, ':password' => $password));
        } else {
            $db_row = $this->db->fetch("SELECT id, username, password, email, admin, follow_own, follow_all, notify_lifecycle, notify_all FROM user JOIN user_notification ON id = user_id WHERE id=:id", array(':id' => $id_or_username));
        }
        if (!empty($db_row)) {
            $this->user = $db_row;
            $result = true;
        }
        return $result;
    } // User::read()

    public function has_follow_own() { return $this->user['follow_own']; }
    public function get_email_notify_new() { // list of all users asking for notifications on new (except me)
        return $this->db->fetch_all("SELECT email FROM user JOIN user_notification ON id = user_id WHERE id != :id AND (notify_lifecycle=1 OR notify_all=1)", array(':id' => $this->user['id']));
    } // User::get_notify_new()
    public function get_id_notify_new() { // list of all users asking for notifications on new
        return $this->db->fetch_all("SELECT email FROM user JOIN user_notification ON id = user_id WHERE id != :id AND (notify_lifecycle=1 OR notify_all=1)", array(':id' => $this->user['id']));
    } // User::get_notify_new()
    public function get_email_notify_solve($issue_id) { // list following users asking for notifications on solve
        return $this->db->fetch_all("SELECT email FROM user JOIN user_notification ON id = user_notification.user_id JOIN issue_notification ON issue_notification.user_id = id WHERE notify_lifecycle=1 OR notify_all=1");
    } // User::get_notify_solve()
    public function get_email_notify_change($issue_id) { // list of all users asking for notifications on changes
    } // User:get_notify_change()

    public function run() {
        switch ($this->environment->get('action')) {
            case 'user_register' :
                $this->register();
                $this->authentication->set_view('login_form');
            break;
        }
    }
} // User

///////////////////
// HELPERS       //
///////////////////

// notify via email: @todo: must be rewritten
function notify($id, $subject, $body){
    global $db;
    $result = sqlite_array_query($db, "SELECT notify_emails FROM issue WHERE id='$id'");
    $to = $result[0]['notify_emails'];
    
    if ($to!=''){
        $headers = "From: ".Configuration::$project['title'].Configuration::$project['email']."\r\n" . 'X-Mailer: PHP/' . phpversion();		
        
        // @xxx notification is disabled for now
        // mail($to, $subject, $body, $headers);	// standard php mail, hope it passes spam filter :)
    }
    
}

function debug($label, $value = null, $backtrace = false) {
    $bt = debug_backtrace();
    $bt =  reset($bt);
    echo('<pre>['.$bt['line'].'] '.$label.(isset($value) ? ":\n".print_r($value, 1) : '').'</pre>'."\n");
    if ($backtrace) {
    echo('<pre>'.print_r(debug_backtrace(), 1).'</pre>'."\n");
    }
}
class Environment {
    protected $field = array();
    public function initialize() {$this->field = $_POST + $_GET; return $this;}
    public function get($field) {return $this->is($field) ? $this->field[$field] : null;}
    public function is($field, $value = null) {return array_key_exists($field, $this->field) && (is_null($value) ? true : $this->field[$field] == $value);}
} // Environment

class DB {
    protected $db;
    public function initialize($name) {
        try {
            $this->db = new PDO("sqlite:$name");
        } catch (PDOException $e) {
            debug('Exception',$e->getMessage());
        }
        // debug('$this->db', $this->$db);
    }
    public function execute($query, $parameter = null) {
        $result = true;
        $error = null;
        if (is_null($parameter)) {
            $error = $this->db->exec($query);
            // debug('error', $error);
        } else {
            if (!is_array($parameter)) {$parameter = array($parameter);}
            // debug('query', $query);
            $statement = $this->db->prepare($query);
            if ($statement) {
                // debug('parameter', $parameter);
                $error = $statement->execute($parameter);
                $this->affected_rows = $statement->rowCount();
            } else {
                $bt =  reset(debug_backtrace());
                debug('error with the query on line '.$bt['line'], $query);
                die();
            }
        }
        $result = ($error !== false);
        return $result;
    }
    public function get_affected_rows() {
        return $this->affected_rows;
    }
    public function get_last_insert_id() {
        return $this->db->lastInsertId();
    }
    public function fetch_all($query, $parameter = array()) {
        $result = array();
        if (!is_array($parameter)) {$parameter = array($parameter);}
        $statement = $this->db->prepare($query);
        // debug('statement', $statement);
        if ($statement) {
            $statement->execute($parameter);
            $result = $statement->fetchAll();
        } else {
            // debug('debug_backtrace', debug_backtrace());
            foreach (array_reverse(debug_backtrace()) as $item) {
                if ($item['class'] == 'DB') {
                    $bt = $item;
                    break;
                }
            }
            debug('error with the query on line '.$bt['line'], $query);
            die();
        }
        return $result;
    } // DB::fetch_all()
    function fetch($query, $parameter = null) {
        $result = $this->fetch_all($query, $parameter);
        if (!empty($result)) {
            $result = $result[0];
        }
        return $result;
    }
    function fetch_field($query, $parameter = null) {
        $result = $this->fetch($query, $parameter);
        return $result[0];
    }
} // DB

class Template {
    var $vars = array();
    var $file = null;

    function set_file($file) {$this->file = $file;}
    /**
     * @todo: always validate the text coming from the user before putting it in a template:
     * you probably want to use striptags() on it.
     */
    function set($name, $value) {
        $this->vars[$name] = $value;
        return $this;
    }

    function fetch($template) {
        if (!empty($this->vars)) {
            extract($this->vars);      // Extract the vars to local namespace
        }
        ob_start();                    // Start output buffering
        if (isset($this->file)) {
            include($this->file);                // Include the file
        } else {
            eval(' ?>'.$template.'<?php '); // ' hack for vim syntax highlight
        }
        $result = ob_get_contents(); // Get the contents of the buffer
        ob_end_clean();                // End buffering and discard
        if (strstr($result, 'eval()\'d code on line') !== false) {
            debug('faulty template', '<ol><li>'.str_replace("\n", "</li><li>", str_replace(array('<', '>'), array('&lt;', '&gt'), $template)).'</li></ol>');
            $error = error_get_last();
            debug('error message', $error['message'].' on line '.$error['line']);
            $bt =  reset(debug_backtrace());
            debug('fetch called on line', str_replace(array('<', '>'), array('&lt;', '&gt'), $bt['line']));
            $result = '';
        }
        return $result; // Return the contents
    }
} // Template

function tr() {
    return Translate::tr(func_get_args());
}
Class Translate {
    static protected $string = array();
    public static function initialize($file) {
        if ($handle = fopen($file, 'r')) {
            while ($data = fgetcsv($handle)) {
                self::$string[$data[0]] = $data[1];
            }
        }
        // debug('string', self::$string);
    }
    public static function tr($arg) {
        if (array_key_exists($arg[0], self::$string)) {
            return count($arg) == 1 ? self::$string[$arg[0]] : vsprintf(self::$string[$arg[0]], array_slice($arg, 1));
        } else {
            return count($arg) == 1 ? $arg[0] : vsprintf($arg[0], array_slice($arg, 1));
        }
    }
} // Translate
